package FileIO;

import model.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.util.*;


/**
 * Levanta los datos de los archivos xml.
 * Procesa los datos en archivos xml.
 *
 * @author c.psivor
 */
public class HidratadorLaberinto {


    private DataLaberinto dataLaberinto;
    private DocumentBuilderFactory dbFactory;

    private static HidratadorLaberinto INSTANCE = null;

    private synchronized static void createInstance() {
        if (INSTANCE == null) {
            INSTANCE = new HidratadorLaberinto();
        }
    }

    public static HidratadorLaberinto getInstance() {
        createInstance();
        return INSTANCE;
    }

    @SuppressWarnings("CloneDoesntCallSuperClone")
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    private HidratadorLaberinto() {
        this.dbFactory = DocumentBuilderFactory.newInstance();
    }

    private String getTagValue(String sTag, Element eElement) {
        return eElement.getAttribute(sTag);
    }

    /**
     * Crea una nueva instancia de laberinto y levanta los datos del xml
     */
    private void procesarLaberintoXML(String archLaberinto) {

        try {
            File fXmlFile = new File(archLaberinto);
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(fXmlFile);

            doc.getDocumentElement().normalize();
            Element elementRaiz = doc.getDocumentElement();
            dataLaberinto = new DataLaberinto(elementRaiz.getAttribute(Constantes.ANCHO), elementRaiz.getAttribute(Constantes.ALTO),
                    elementRaiz.getAttribute(Constantes.NODO_ANCHO), elementRaiz.getAttribute(Constantes.NODO_ALTO),
                    elementRaiz.getAttribute(Constantes.INICIO_PACMAN), elementRaiz.getAttribute(Constantes.INICIO_FANTASMAS));

            NodeList nodeList = elementRaiz.getElementsByTagName(Constantes.NODO);
            Collection<DataNodo> colDataNodos = new ArrayList<DataNodo>();

            for (int temp = 0; temp < nodeList.getLength(); temp++) {

                Node nNode = nodeList.item(temp);

                if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                    Element eElement = (Element) nNode;
                    DataNodo dataNodo = new DataNodo(getTagValue(Constantes.ID, eElement), getTagValue(Constantes.FILA, eElement), getTagValue(Constantes.COLUMNA, eElement),
                            getTagValue(Constantes.CONTIENE, eElement), getTagValue(Constantes.IZQUIERDA_LC, eElement), getTagValue(Constantes.DERECHA_LC, eElement),
                            getTagValue(Constantes.ARRIBA_LC, eElement), getTagValue(Constantes.ABAJO_LC, eElement));
                    colDataNodos.add(dataNodo);
                }
            }
            dataLaberinto.setNodos(colDataNodos);
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Arma un mapa de filas con sus respectivas columnas
     */
    private Map<Integer, List<DataNodo>> crearMapaNodosFilaColumnas() {
		Map<Integer, List<DataNodo>> mapaNodosFilaColumnas=new HashMap<Integer,List<DataNodo>>();
        for (DataNodo columna : dataLaberinto.getNodos()) {
            if (mapaNodosFilaColumnas.containsKey(Integer.parseInt(columna.getFila()))) {
                mapaNodosFilaColumnas.get(Integer.parseInt(columna.getFila())).add(columna);
            } else {
                List<DataNodo> columnas = new ArrayList<DataNodo>();
                columnas.add(columna);
                mapaNodosFilaColumnas.put(Integer.parseInt(columna.getFila()), columnas);
            }

        }
	
		return mapaNodosFilaColumnas;
	}
    
    /**
     * Construye el elemento del escenario según los datos de nodo correspondientes al archivo.
     * Si los sentidos arriba, abajo, derecha e izquierda estan vacios el elemento será Pared, caso
     * contrario será un camino con bolita (por default) o si contiene Bolon sera un camino que incluya 
     * este item.
     * La dimension que se le asigna a cada elemento se desplazará en 1 dado que se diseño un contorno para 
     * definir los teletransportadores.
     * 
     * @return ElemEscenario
     */
    private ElemEscenario construirElementoParedOCamino(DataNodo nodo){
    	ElemEscenario elemEscenario;
    	int y = Integer.parseInt(nodo.getFila())+1;
    	int x = Integer.parseInt(nodo.getColumna())+1;
    	    	
    	if (nodo.getArriba().equalsIgnoreCase("") && nodo.getAbajo().equalsIgnoreCase("") && nodo.getDerecha().equalsIgnoreCase("")
    		&& nodo.getIzquierda().equalsIgnoreCase("")){
    		elemEscenario=new Pared(y,x);
    	}else{
    		if(nodo.getContiene().equalsIgnoreCase("BOLITA")){
    			elemEscenario=new Camino(y,x);
    		}else if(nodo.getContiene().equalsIgnoreCase("BOLON")){
    			elemEscenario=new Camino(y,x,new Bolon());
    		}else{
    			elemEscenario = new Camino(y,x);
    		}
    	}
    	   	
    	return elemEscenario;
    }
    
    /**
     * Si los elementos que estan en los bordes son caminos se devolverá true
     * caso contrario devuelve false.
     * 
     */
    private boolean construirTransportadorEnBorde(ElemEscenario elementoBorde1,ElemEscenario elementoBorde2){

        return (elementoBorde1 instanceof Camino) && (elementoBorde2 instanceof Camino);

    }
    
    private void actualizarElementosHorizontales(ArrayList<ElemEscenario> filaHorizontalArriba,ArrayList<ElemEscenario> filaHorizontalAbajo){
    	
    	ArrayList<ElemEscenario> filaHorizontalArribaAux=new ArrayList<ElemEscenario>();
    	ArrayList<ElemEscenario> filaHorizontalAbajoAux=new ArrayList<ElemEscenario>();
    	
    	for (int i=0;i<filaHorizontalArriba.size();i++) { 
    		ElemEscenario elemFilaHorizontalArriba = filaHorizontalArriba.get(i);
    		ElemEscenario elemFilaHorizontalAbajo = filaHorizontalAbajo.get(i);
			boolean isTransportador=construirTransportadorEnBorde(elemFilaHorizontalArriba,elemFilaHorizontalAbajo);
						
			if(isTransportador){
				filaHorizontalArribaAux.add(new Transportador(elemFilaHorizontalArriba.getY()-1,elemFilaHorizontalArriba.getX(),elemFilaHorizontalAbajo.getY(),elemFilaHorizontalAbajo.getX()));
				filaHorizontalAbajoAux.add(new Transportador(elemFilaHorizontalAbajo.getY()+1,elemFilaHorizontalAbajo.getX(),elemFilaHorizontalArriba.getY(),elemFilaHorizontalArriba.getX()));
			}else{
                //Forzar que sea pared! Sino puede ser camino!
				filaHorizontalArribaAux.add(new Pared(elemFilaHorizontalArriba.getY()-1,elemFilaHorizontalArriba.getX()));
				filaHorizontalAbajoAux.add(new Pared(elemFilaHorizontalAbajo.getY()+1,elemFilaHorizontalAbajo.getX()));
			}
		}
    	    	    	
    	filaHorizontalArriba.clear();
    	filaHorizontalArriba.addAll(filaHorizontalArribaAux);
    	
    	filaHorizontalAbajo.clear();
    	filaHorizontalAbajo.addAll(filaHorizontalAbajoAux);
    }

    //Columnas!
    private void construirContornoHorizontal(ArrayList<ArrayList<ElemEscenario>> laberinto){
    	ArrayList<ElemEscenario> columnaCero=new ArrayList<ElemEscenario>();
    	ArrayList<ElemEscenario> columnaN=new ArrayList<ElemEscenario>();
    	ArrayList<ArrayList<ElemEscenario>> laberintoAux = new ArrayList<ArrayList<ElemEscenario>>();
    	
    	columnaCero.addAll(laberinto.get(0));
    	columnaN.addAll(laberinto.get(laberinto.size()-1));
    
    	actualizarElementosHorizontales(columnaCero,columnaN);
  	
    	laberintoAux.addAll(laberinto);
    	laberinto.clear();
    	laberinto.add(columnaCero);
    	laberinto.addAll(laberintoAux);
    	laberinto.add(columnaN);
    }   
    
    private void construirContornoVertical(ArrayList<ArrayList<ElemEscenario>> laberinto) {
		ArrayList<ArrayList<ElemEscenario>> laberintoConContorno = new ArrayList<ArrayList<ElemEscenario>>();

        for (ArrayList<ElemEscenario> filas : laberinto) { //Obtengo todas las filas
            ArrayList<ElemEscenario> filaConContorno = new ArrayList<ElemEscenario>();
            ElemEscenario elemPrimerFila = filas.get(0).clone();
            ElemEscenario elemUltimaFila = filas.get(filas.size() - 1).clone();
            boolean isTransportador = construirTransportadorEnBorde(elemPrimerFila, elemUltimaFila);
            elemPrimerFila.setX(elemPrimerFila.getX() - 1);
            elemPrimerFila.setY(elemPrimerFila.getY());
            elemUltimaFila.setX(elemUltimaFila.getX() + 1);
            elemUltimaFila.setY(elemUltimaFila.getY());

            if (isTransportador) {
                //Fixme - los x e y del transportador van a un camino, no a otro transportador.
                filaConContorno.add(new Transportador(elemPrimerFila.getY(), elemPrimerFila.getX(), elemUltimaFila.getY(), elemUltimaFila.getX() - 1));
                filaConContorno.addAll(filas);
                filaConContorno.add(new Transportador(elemUltimaFila.getY(), elemUltimaFila.getX(), elemPrimerFila.getY(), elemPrimerFila.getX() + 1));
            } else {
                filaConContorno.add(new Pared(elemPrimerFila.getY(),elemPrimerFila.getX()));
                filaConContorno.addAll(filas);
                filaConContorno.add(new Pared(elemUltimaFila.getY(),elemUltimaFila.getX()));
            }
            laberintoConContorno.add(filaConContorno);
        }
    	laberinto.clear();
    	laberinto.addAll(laberintoConContorno);
	}
    
    /**
     * Actualiza el laberinto agregandole el contorno donde se encontraran los teletransportadores
     * Observaciones: 	Si en los bordes hay un camino, al lado corresponde un Transportador
     *   				Si en los bordes hay una pared, al lado va una pared..
     *   				Si la dim es x, al momento de crear la dim sera x+1 (IDEM y)
     */
    private void agregarContornoLaberinto(ArrayList<ArrayList<ElemEscenario>> laberinto){
    	construirContornoVertical(laberinto);
    	construirContornoHorizontal(laberinto);
  	}
  
   

	/**
     * Construye el laberinto trazando las columnas correspondientes a cada fila
     */
    private ArrayList<ArrayList<ElemEscenario>> construirLaberinto(ElemEscenario inicioPacman,ElemEscenario inicioFantasmas){
    	ArrayList<ArrayList<ElemEscenario>> laberinto = new ArrayList<ArrayList<ElemEscenario>>();
    	Map<Integer,List<DataNodo>> mapaNodosFilaColumnas = crearMapaNodosFilaColumnas();  
    	
    	List<Integer> keys = new ArrayList<Integer>();
    	keys.addAll(mapaNodosFilaColumnas.keySet());
    	Collections.sort(keys);
    	
    	for (Integer key : keys) {
    		List<DataNodo> columnas = mapaNodosFilaColumnas.get(key);
    		ArrayList<ElemEscenario> columna = new ArrayList<ElemEscenario>();
    		
            for (DataNodo columna1 : columnas) {
                if ((Integer.parseInt(columna1.getFila()+1) == inicioPacman.getY()) && (Integer.parseInt(columna1.getColumna()+1) == inicioPacman.getX())) {
                    columna.add(inicioPacman);
                } else if ((Integer.parseInt(columna1.getFila()+1) == inicioFantasmas.getY()) && (Integer.parseInt(columna1.getColumna()+1) == inicioFantasmas.getX())) {
                    columna.add(inicioFantasmas);
                } else {
                    columna.add(construirElementoParedOCamino(columna1));
                }
             }
    		laberinto.add(columna);
    	
    	}
    	
    	return laberinto;
    	
    }
    
   

	public ArrayList<ArrayList<ElemEscenario>> crearEscenario(Entero y, Entero x, ElemEscenario inicioPacman, ElemEscenario inicioFantasmas,Cadena nodoAlto,Cadena nodoAncho) {
        //POR DEFAULT EL CAMINO INSTANCIA UNA BOLITA
        //Suponemos que hay una bolita en cada camino siempre...Se instancia en el constructor por default

		this.procesarLaberintoXML(AppConfig.getInstance().getPathArchivoLaberinto());


        nodoAlto.setStringValue(dataLaberinto.getNodoAlto());
        nodoAncho.setStringValue(dataLaberinto.getNodoAncho());
        y.setIntValue(Integer.parseInt(dataLaberinto.getAncho()) + 2);
        x.setIntValue(Integer.parseInt(dataLaberinto.getAlto()) + 2);
        int yInicioFantasmas = getYCoorFromString(dataLaberinto.getInicioFantasmas())+1;
        int yInicioPacman = getYCoorFromString(dataLaberinto.getInicioPacman())+1;
        int xInicioFantasmas = getXCoorFromString(dataLaberinto.getInicioFantasmas())+1;
        int xInicioPacman = getXCoorFromString(dataLaberinto.getInicioPacman())+1;

        inicioPacman.setY(yInicioPacman);
        inicioPacman.setX(xInicioPacman);
        inicioFantasmas.setY(yInicioFantasmas);
        inicioFantasmas.setX(xInicioFantasmas);
        
        ArrayList<ArrayList<ElemEscenario>> laberinto = construirLaberinto(inicioPacman,inicioFantasmas);
        agregarContornoLaberinto(laberinto);

        return laberinto;
    }

    private int getYCoorFromString(String coord) {
        // (Y-X)
        if (coord.length() == 4) {
            return Integer.valueOf(coord.substring(0, 2));
        } else if (coord.length() == 2) {
            return Integer.valueOf(coord.substring(0, 1));
        }
        return -1;
    }

    private int getXCoorFromString(String coord) {
        // (Y-X)
        if (coord.length() == 4) {
            return Integer.valueOf(coord.substring(2));
        } else if (coord.length() == 2) {
            return Integer.valueOf(coord.substring(1));
        }
        return -1;
    }

}
